/*
 * PipelineModelBuilder.java
 *
 * Created on August 1, 2006, 10:07 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package com.smallx.xproc;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Location;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;

/**
 *
 * @author alex
 */
public class PipelineModelBuilder {

   public static final URI NAMESPACE = URI.create("http://www.w3.org/2007/03/xproc");
   static final Name PIPELINE_NM = Name.create(NAMESPACE,"pipeline");
   static final Name PIPELINE_LIBRARY_NM = Name.create(NAMESPACE,"pipeline-library");
   static final Name DECLARE_INPUT_NM = Name.create(NAMESPACE,"input");
   static final Name DECLARE_OUTPUT_NM = Name.create(NAMESPACE,"output");
   static final Name DECLARE_COMPONENT_NM = Name.create(NAMESPACE,"declare-step");
   static final Name DECLARE_PARAMETER_NM = Name.create(NAMESPACE,"parameter");
   static final Name PIPE_NM = Name.create(NAMESPACE,"pipe");
   static final Name DOCUMENT_NM = Name.create(NAMESPACE,"document");
   static final Name INLINE_NM = Name.create(NAMESPACE,"inline");
   static final Name IMPORT_NM = Name.create(NAMESPACE,"import");
   static final Name PARAMETER_NM = Name.create(NAMESPACE,"parameter");
   static final Name INPUT_NM = Name.create(NAMESPACE,"input");
   static final Name IGNORE_PREFIXES_NM = Name.create(NAMESPACE,"ignore-prefixes");
   //static final Name STEP_NM = Name.create(NAMESPACE,"step");
   static final Name FOREACH_NM = Name.create(NAMESPACE,"for-each");
   static final Name ITERATION_SOURCE_NM = Name.create(NAMESPACE,"iteration-source");
   static final Name VIEWPORT_NM = Name.create(NAMESPACE,"viewport");
   static final Name VIEWPORT_SOURCE_NM = Name.create(NAMESPACE,"viewport-source");
   static final Name IMPORT_PARAMETER_NM = Name.create(NAMESPACE,"import-parameter");
   static final Name CURRENT_NM = Name.create("current");
   
   static private final String STEP_REF_NAME = "step";
   static private final String PORT_REF_NAME = "port";
   static private final String HREF_LINK_NAME = "href";
   static private final String PORT_NAME_NAME = "port";
   static private final String COMPONENT_NAME_NAME = "type";
   static private final String STEP_NAME_NAME = "name";

   static PipelineModel builtInModel = null;
   static {
      try {
         URL url = PipelineModelBuilder.class.getResource("standard-library.xml");
         PipelineModelBuilder builder = new PipelineModelBuilder();
         Document doc = builder.getDocumentLoader().load(url.toURI());
         builtInModel = builder.build(doc);
      } catch (Exception ex) {
         throw new RuntimeException("Cannot intitial static model for built-in components.",ex);
      }
   }
   
   protected interface ElementHandler {
      void handle(BuildContext build,Element decl);
   }
   
   class PipelineContainerHandler extends StepContainerHandler {
      PipelineContainerHandler(PipelineModel model,StepContainer container) {
         super(model,container);
      }
      public void handle(BuildContext build,Element decl) {
         boolean inSteps = false;
         if (inSteps) {
            super.handle(build,decl);
         } else if (decl.getName().equals(DECLARE_COMPONENT_NM)) {
            parseComponentDeclaration(build,model,decl);
         } else if (decl.getName().equals(IMPORT_NM)) {
            parseImport(build,model,decl);
         } else {
            inSteps = true;
            super.handle(build,decl);
         }
      }
   }
   
   class StepContainerHandler implements ElementHandler {
      PipelineModel model;
      StepContainer container;
      StepContainerHandler(PipelineModel model,StepContainer container) {
         this.container = container;
         this.model = model;
      }
      public void handle(BuildContext build,Element decl) {
         if (decl.getName().equals(VIEWPORT_NM)) {
            String nameString = decl.getAttributeValue(STEP_NAME_NAME);
            if (nameString==null) {
               build.errors.addError("The '"+STEP_NAME_NAME+"' attribute is missing.",decl);
               return;
            }
            Name name;
            try {
               name = decl.getNamespaceScope().expandName(nameString);
            } catch (XMLException xex) {
               build.errors.addError("Cannot expand name QName: "+xex.getMessage(),decl);
               return;
            }
            String xpath = decl.getAttributeValue("match");
            if (xpath==null) {
               build.errors.addError("The 'match' attribute is missing.",decl);
               return;
            }
            Viewport viewport = new Viewport(name,container,new Expression(xpath,decl.getNamespaceScope()));
            if (container.getStep(name)!=null) {
               build.errors.addError("A step with name "+name+" already exists.",decl);
               return;
            }
            parseViewport(build,viewport,decl,new StepContainerHandler(model,viewport));
            try {
               container.addStep(viewport);
            } catch (ModelErrorException ex) {
               build.errors.addError(ex.getMessage(),decl);
            }
         } else if (decl.getName().equals(FOREACH_NM)) {
            String nameString = decl.getAttributeValue(STEP_NAME_NAME);
            if (nameString==null) {
               build.errors.addError("The '"+STEP_NAME_NAME+"' attribute is missing.",decl);
               return;
            }
            Name name;
            try {
               name = decl.getNamespaceScope().expandName(nameString);
            } catch (XMLException xex) {
               build.errors.addError("Cannot expand name QName: "+xex.getMessage(),decl);
               return;
            }
            ForEach foreach = new ForEach(name,container);
            if (container.getStep(name)!=null) {
               build.errors.addError("A step with name "+name+" already exists.",decl);
               return;
            }
            parseForeach(build,foreach,decl,new StepContainerHandler(model,foreach));
            try {
               container.addStep(foreach);
            } catch (ModelErrorException ex) {
               build.errors.addError(ex.getMessage(),decl);
            }
         } else if (!build.ignored.contains(decl.getName().getNamespaceName())) {
            parseStep(build,model,container,decl);
            /*
         } else {
            errors.addError("Unrecognized element "+decl.getName()+" in container "+container.getName(),decl);
             */
         }
      }
   }
   
   static class BuildContext {
      ModelErrorException errors;
      Set<URI> ignored;
      BuildContext() {
         errors = new ModelErrorException("Invalid pipeline.");
         ignored = new HashSet<URI>();
      }
   }
   
   DocumentLoader loader;
   
   /** Creates a new instance of PipelineModelBuilder */
   public PipelineModelBuilder() {
      this.loader = new SAXDocumentLoader();
   }
   
   public void setDocumentLoader(DocumentLoader loader) {
      this.loader = loader;
   }
   
   public DocumentLoader getDocumentLoader() {
      return this.loader;
   }
   
   public PipelineModel build(Document doc) 
      throws ModelErrorException
   {
      return build(doc.getDocumentElement());
   }
   
   public PipelineModel build(Element pipe) 
      throws ModelErrorException
   {
      if (!pipe.getName().equals(PIPELINE_NM) && !pipe.getName().equals(PIPELINE_LIBRARY_NM)) {
         ModelErrorException ex = new ModelErrorException("Invalid pipeline.");
         ex.addError("Unexpected root element "+pipe.getName()+" for pipeline, expecting a "+PIPELINE_NM+" element.",pipe);
         throw ex;
      }
      String nameString = pipe.getAttributeValue(STEP_NAME_NAME);
      if (nameString==null) {
         ModelErrorException ex = new ModelErrorException("Invalid pipeline.");
         ex.addError("The '"+STEP_NAME_NAME+"' attribute is missing on the "+PIPELINE_NM+" element.",pipe);
         throw ex;
      }
      Name name;
      try {
         name = pipe.getNamespaceScope().expandName(nameString);
      } catch (XMLException ex) {
         throw new ModelErrorException("Cannot expand pipeline name: "+ex.getMessage(),ex); 
      }
      
      PipelineModel model = new PipelineModel(name);
      model.setLocation(pipe.getBaseURI(),(Location)pipe);
      if (builtInModel!=null) {
         model.importComponents(builtInModel.getComponentTypes());
      }
         
      BuildContext build = new BuildContext();
      if (pipe.getName().equals(PIPELINE_NM)) {
         //model.getPipelineGroup().addStep(model.getPipelineGroup());
         String excludedPrefixes = pipe.getAttributeValue("exclude-result-prefixes");
         if (excludedPrefixes!=null) {
            Set<URI> excluded = model.getExcludedNamespaces();
            String [] prefixes = excludedPrefixes.split("\\s+");
            if (prefixes!=null) {
               for (int i=0; i<prefixes.length; i++) {
                  URI ns = pipe.getNamespaceScope().getNamespace(prefixes[i]);
                  if (ns==null) {
                     build.errors.addError("Cannot find namespace for excluded prefix "+prefixes[i],pipe);
                  } else {
                     excluded.add(ns);
                  }
               }
            }
         }
         String ignorePrefixes = pipe.getAttributeValue(IGNORE_PREFIXES_NM);
         Set<URI> ignored = new HashSet<URI>();
         if (ignorePrefixes!=null) {
            String [] prefixes = excludedPrefixes.split("\\s+");
            if (prefixes!=null) {
               for (int i=0; i<prefixes.length; i++) {
                  URI ns = pipe.getNamespaceScope().getNamespace(prefixes[i]);
                  if (ns==null) {
                     build.errors.addError("Cannot find namespace for ignoreable prefix "+prefixes[i],pipe);
                  } else {
                     ignored.add(ns);
                  }
               }
            }
         }

         parseContainer(build,model.getPipelineGroup(),pipe,new PipelineContainerHandler(model,model.getPipelineGroup()));
         
         
      } else {
         parseLibrary(build,model,pipe);
      }

      if (build.errors.getErrors().hasNext()) {
         throw build.errors;
      }
      
      return model;
   }
   
   protected Reference parseInput(BuildContext build,Element inputDecl, boolean allowSelect) {
      Expression expr = null;
      String selectExpr = inputDecl.getAttributeValue("select");
      if (selectExpr!=null) {
         expr = new Expression(selectExpr,inputDecl.getNamespaceScope());
      }
      Reference theReference = null;
      for (Iterator<Element> children = inputDecl.getElementChildren(); children.hasNext(); ) {
         Element child = children.next();
         if (child.getName().equals(PIPE_NM)) {
            if (theReference!=null) {
               build.errors.addError("The input already has a value.",child);
               return null;
            }
            String portString = child.getAttributeValue(PORT_REF_NAME);
            if (portString==null) {
               build.errors.addError("The "+PORT_REF_NAME+" attribute is missing.",child);
               return null;
            }
            Name portName;
            try {
               portName = child.getNamespaceScope().expandName(portString);
            } catch (XMLException pex) {
               build.errors.addError("Cannot parse port reference: "+pex.getMessage(),inputDecl);
               return null;
            }
            if (portName.getNamespaceName()!=Name.NO_NAMESPACE) {
               build.errors.addError("Port names cannot have namespaces.",inputDecl);
               return null;
            }
            String stepString = child.getAttributeValue(STEP_REF_NAME);
            if (stepString==null) {
               build.errors.addError("The "+STEP_REF_NAME+" attribute is missing.",child);
               return null;
            }
            Name stepName;
            try {
               stepName = child.getNamespaceScope().expandName(stepString);
            } catch (XMLException pex) {
               build.errors.addError("Cannot parse step reference: "+pex.getMessage(),inputDecl);
               return null;
            }
            if (stepName.getNamespaceName()!=Name.NO_NAMESPACE) {
               build.errors.addError("Step names cannot have namespaces.",inputDecl);
               return null;
            }

            theReference = new PortReference(stepName,portName);
            
         } else if (child.getName().equals(INLINE_NM)) {
            if (theReference!=null) {
               build.errors.addError("The input already has a value.",child);
               return null;
            }
            Iterator<Element> inlineValue = child.getElementChildren();
            if (!inlineValue.hasNext()) {
               build.errors.addError("The inline document is empty.",child);
               return null;
            }
            Element documentElement = inlineValue.next();
            if (inlineValue.hasNext()) {
               build.errors.addError("The inline document has more than one element child.",child);
               return null;
            }
            try {
               Document doc = documentElement.getInfoset().createItemConstructor().createDocument(documentElement.getBaseURI());
               // We shouldn't localize cause that does really bad things for 
               // here documents as literal results
               documentElement.localizeNamespaceDeclarations();
               Element root = (Element)documentElement.copyOfItem(true);
               doc.add(root);
               theReference = new InlineDocument(doc);
            } catch (XMLException xex) {
               build.errors.addError("Cannot create input document: "+xex.getMessage(),inputDecl);
               return null;
            }
         } else if (child.getName().equals(DOCUMENT_NM)) {
            if (theReference!=null) {
               build.errors.addError("The input already has a value.",child);
               return null;
            }
            String hrefString = child.getAttributeValue(HREF_LINK_NAME);
            if (hrefString==null) {
               build.errors.addError("The "+HREF_LINK_NAME+" attribute is missing on the "+DOCUMENT_NM+" element.",child);
               return null;
            }
            URI location = child.getBaseURI().resolve(hrefString);
            theReference = new StaticReference(location);
         } else if (child.getName().getNamespaceName().equals(NAMESPACE)) {
            build.errors.addError("The element "+child+" is not allowed in "+inputDecl.getName(),inputDecl);
            return null;
         }
      }
      return expr==null ? theReference : new ComputedPort(theReference,expr);
   }
   
   protected PortDefinition parseInputDeclaration(BuildContext build,Element inputDecl) {
      String portString = inputDecl.getAttributeValue(PORT_NAME_NAME);
      if (portString==null) {
         build.errors.addError("Missing '"+PORT_NAME_NAME+"' attribute on an input declaration.",inputDecl.getBaseURI(),inputDecl);
         return null;
      }
      portString = portString.trim();
      Name pname = null;
      if (!portString.equals("*")) {
         try {
            pname = inputDecl.getNamespaceScope().expandName(portString);
         } catch (XMLException xex) {
            build.errors.addError("Cannot expand port QName: "+xex.getMessage(),inputDecl);
            return null;
         }
         if (pname.getNamespaceName()!=Name.NO_NAMESPACE) {
            build.errors.addError("Port names cannot have a namespace",inputDecl);
            return null;
         }
      }
      Reference ref = null;
      if (inputDecl.size()>0 && (inputDecl.getElementsByName(PIPE_NM).hasNext() ||
                                 inputDecl.getElementsByName(INLINE_NM).hasNext() ||
                                 inputDecl.getElementsByName(DOCUMENT_NM).hasNext()) ) {
         ref = parseInput(build,inputDecl,true);
         if (ref==null) {
            return null;
         }
      }
      boolean allowSequence = "yes".equals(inputDecl.getAttributeValue("sequence"));
      return new PortDefinition(pname,allowSequence,ref);
   }
   
   protected PortDefinition parseOutputDeclaration(BuildContext build,Element outputDecl) {
      PortDefinition def = parseInputDeclaration(build,outputDecl);
      if (def!=null && def.getName()==null) {
         build.errors.addError("Output port definitions cannot have wildcard names.",outputDecl);
         return null;
      }
      return def;
   }
   
   protected void parseComponentDeclaration(BuildContext build,PipelineModel model,Element decl) {
      String nameString = decl.getAttributeValue(STEP_NAME_NAME);
      Name name = null;
      if (nameString==null) {
         build.errors.addError("Component declarations must have a '"+STEP_NAME_NAME+"' attribute.",decl);
      }
      try {
         name = decl.getNamespaceScope().expandName(nameString);
      } catch (XMLException xex) {
         build.errors.addError("Cannot expand name QName: "+xex.getMessage(),decl);
         return;
      }
      ComponentType type = new ComponentType(name);
      Iterator<Element> children = decl.getElementChildren();
      while (children.hasNext()) {
         Element child = children.next();
         Name cname = child.getName();
         if (cname.equals(DECLARE_INPUT_NM)) {
            PortDefinition def = parseInputDeclaration(build,child);
            if (def!=null) {
               if (def.getName()==null) {
                  type.setArbitraryInputs(true);
               } else {
                  type.addInputPort(def.getName(),def.isSequenceAllowed());
               }
               if (def.getReference()!=null) {
                  build.errors.addError("Component definitions cannot have port references or here documents.",child);
               }
            }
         } else if (cname.equals(DECLARE_OUTPUT_NM)) {
            PortDefinition def = parseOutputDeclaration(build,child);
            if (def!=null) {
               type.addOutputPort(def.getName(),def.isSequenceAllowed());
               if (def.getReference()!=null) {
                  build.errors.addError("Component definitions cannot have port references or here documents.",child);
               }
            }
         } else if (cname.equals(DECLARE_PARAMETER_NM)) {
         } else {
            build.errors.addError("The element "+cname+" is not allowed in a step declaration.",child);
         }
      }
      try {
         model.addComponentType(type);
      } catch (ModelErrorException ex) {
         build.errors.addError(ex.getMessage(),decl);
      }
   }
   
   protected void parseLibrary(BuildContext build,PipelineModel model,Element libraryDecl) {
      String lnameString = libraryDecl.getAttributeValue(STEP_NAME_NAME);
      Name lname = null;
      if (lnameString!=null) {
         try {
            lname = libraryDecl.getNamespaceScope().expandName(lnameString);
         } catch (XMLException xex) {
            build.errors.addError("Cannot expand name QName: "+xex.getMessage(),libraryDecl);
            return;
         }
      }
      
      Iterator<Element> children = libraryDecl.getElementChildren();
      while (children.hasNext()) {
         Element child = children.next();
         Name cname = child.getName();
         if (cname.equals(IMPORT_NM)) {
            parseImport(build,model,child);
         } else if (cname.equals(DECLARE_COMPONENT_NM)) {
            parseComponentDeclaration(build,model,child);
         } else if (cname.equals(PIPELINE_NM)) {
            PipelineModelBuilder builder = new PipelineModelBuilder();
            builder.setDocumentLoader(loader);
            try {
               PipelineModel subModel = builder.build(child);
               // Now what ?!?
            } catch (ModelErrorException ex) {
               Iterator<ModelErrorException.Error> iter = ex.getErrors();
               while (iter.hasNext()) {
                  ModelErrorException.Error error = iter.next();
                  build.errors.addError(error.getMessage(),error.getBaseURI(),error.getLine(),error.getColumn());
               }
            }
         } else if (cname.getNamespaceName().equals(NAMESPACE)) {
            build.errors.addError("The element "+cname+" is not allowed in a pipeline library.",child);
         }
      }
   }
   
   protected void parseImport(BuildContext build,PipelineModel model,Element decl) {
      String hrefString = decl.getAttributeValue(HREF_LINK_NAME);
      if (hrefString==null) {
         build.errors.addError("No '"+HREF_LINK_NAME+" specified on the import.",decl);
      }
      URI url = decl.getBaseURI().resolve(hrefString);
      try {
         Document doc = loader.load(url);
         Element library = doc.getDocumentElement();
         if (!library.getName().equals(PIPELINE_LIBRARY_NM)) {
            build.errors.addError("Unrecognized document element name "+library.getName(),decl);
            return;
         }
         parseLibrary(build,model,library);
      } catch (XMLException ex) {
         build.errors.addError("XML error on imported library "+url+" : "+ex.getMessage(),decl);
      } catch (IOException ex) {
         build.errors.addError("I/O error on imported library "+url+" : "+ex.getMessage(),decl);
      }
   }
   
   protected void parseStep(BuildContext build,PipelineModel model,StepContainer container,Element stepDecl) {
      String snameString = stepDecl.getAttributeValue(STEP_NAME_NAME);
      if (snameString==null) {
         build.errors.addError("The '"+STEP_NAME_NAME+"' attribute is missing.",stepDecl);
         return;
      }
      Name sname;
      try {
         sname = stepDecl.getNamespaceScope().expandName(snameString);
      } catch (XMLException xex) {
         build.errors.addError("Cannot expand name QName: "+xex.getMessage(),stepDecl);
         return;
      }
      if (sname.getNamespaceName()!=Name.NO_NAMESPACE) {
         build.errors.addError("Step names cannot have namespaces: "+sname,stepDecl);
         return;
      }
      /*
      String kindString = stepDecl.getAttributeValue(COMPONENT_NAME_NAME);
      if (kindString==null) {
         build.errors.addError("The '"+COMPONENT_NAME_NAME+"' attribute is missing.",stepDecl);
         return;
      }
      Name kind;
      try {
         kind = stepDecl.getNamespaceScope().expandName(kindString);                      
      } catch (XMLException xex) {
         build.errors.addError("Cannot expand name QName: "+xex.getMessage(),stepDecl);
         return;
      }*/
      Name kind = stepDecl.getName();
      try {

         ComponentType type = model.getComponentType(kind);
         if (type==null) {
            build.errors.addError("Cannot find component declaration for type "+kind,stepDecl);
            return;
         }
         Step step = container.createStep(type,sname,stepDecl.getNamespaceScope());
         
         Iterator<Element> children = stepDecl.getElementChildren();
         Map<Name,String> parameters = step.getParameters();
         boolean portsEnded = false;
         while (children.hasNext()) {
            Element part = children.next();
            if (part.getName().equals(INPUT_NM)) {
               String portString = part.getAttributeValue(PORT_NAME_NAME);
               if (portString==null) {
                  build.errors.addError("Missing '"+PORT_NAME_NAME+"' attribute on an input declaration.",part.getBaseURI(),part);
                  continue;
               }
               Name pname;
               try {
                  pname = part.getNamespaceScope().expandName(portString);
               } catch (XMLException xex) {
                  build.errors.addError("Cannot expand port QName: "+xex.getMessage(),part);
                  continue;
               }
               if (pname.getNamespaceName()!=Name.NO_NAMESPACE) {
                  build.errors.addError("Port names cannot have a namespace",part);
                  continue;
               }
               Reference ref = parseInput(build,part,true);
               if (ref!=null) {
                  step.bindInput(pname,ref);
               }
               
            } else if (part.getName().equals(IMPORT_PARAMETER_NM)) {
               build.errors.addError(part.getName()+" is not supported yet.",part.getBaseURI(),part);
            } else if (part.getName().equals(PARAMETER_NM)) {
               String pnameString = part.getAttributeValue("name");
               if (pnameString==null) {
                  build.errors.addError("Missing 'name' attribute on parameter declaration.",part.getBaseURI(),part);
                  continue;
               }
               Name pname;
               try {
                  pname = part.getNamespaceScope().expandName(pnameString);
               } catch (XMLException xex) {
                  build.errors.addError("Cannot expand parameter QName: "+xex.getMessage(),part);
                  continue;
               }
               String value = part.getAttributeValue("value");
               if (value==null) {
                  value = "";
               }
               parameters.put(pname,value);
            } else {
               // toss, not an error
            }
         }
      } catch (ModelErrorException mex) {
         build.errors.addError(mex.getMessage(),stepDecl);
      }
   }
   
   protected void parseContainer(BuildContext build,StepContainer stepContainer,Element containerElement,ElementHandler handler) {
      Iterator<Element> children = containerElement.getElementChildren();
      Map<Name,String> parameters = stepContainer.getParameters();
      boolean prologEnded = false;
      while (children.hasNext()) {
         Element decl = children.next();
         if (decl.getName().equals(DECLARE_INPUT_NM)) {
            if (prologEnded) {
               build.errors.addError(decl.getName()+" out of place.",decl.getBaseURI(),decl);
               continue;
            }
            PortDefinition def = parseInputDeclaration(build,decl);
            if (def.getName()==null) {
               build.errors.addError("Wildcard declarations are not allowed in step containers.",decl);
               continue;
            }
            try {
               stepContainer.defineInputPort(def);
            } catch (ModelErrorException ex) {
               build.errors.addError(ex.getMessage(),decl);
            }
         } else if (decl.getName().equals(DECLARE_OUTPUT_NM)) {
            if (prologEnded) {
               build.errors.addError(decl.getName()+" out of place.",decl);
               continue;
            }
            PortDefinition def = parseOutputDeclaration(build,decl);
            if (def==null) {
               build.errors.addError("No port definition was found.",decl);
               continue;
            }
            try {
               stepContainer.defineOutputPort(def);
            } catch (ModelErrorException ex) {
               build.errors.addError(ex.getMessage(),decl);
            }
         } else if (decl.getName().equals(PARAMETER_NM)) {
            String nameString = decl.getAttributeValue("name");
            if (nameString==null) {
               build.errors.addError("Missing 'name' attribute on parameter declaration.",decl.getBaseURI(),decl);
               continue;
            }
            Name name;
            try {
               name = decl.getNamespaceScope().expandName(nameString);
            } catch (XMLException xex) {
               build.errors.addError("Cannot expand parameter QName: "+xex.getMessage(),decl);
               continue;
            }
            String value = decl.getAttributeValue("value");
            if (value==null) {
               value = "";
            }
            parameters.put(name,value);
         } else {
            prologEnded = true;
            handler.handle(build,decl);
         }
      }
   }
   
   protected void parseViewport(BuildContext build,Viewport viewport,Element containerElement,ElementHandler handler) {
      Iterator<Element> children = containerElement.getElementChildren();
      Map<Name,String> parameters = viewport.getParameters();
      
      // viewport-source
      if (!children.hasNext()) {
         build.errors.addError("Missing a "+VIEWPORT_SOURCE_NM+" element.");
         return;
      }
      Element source = children.next();
      if (!source.getName().equals(VIEWPORT_SOURCE_NM)) {
         build.errors.addError("Expecting a "+VIEWPORT_SOURCE_NM+" element but found "+source.getName()+" instead.");
         return;
      }
      Reference ref = parseInput(build,source,false);
      if (ref==null) {
         build.errors.addError("No input definition was found.",source);
         return;
      }
      PortDefinition def = new PortDefinition(CURRENT_NM,false,ref);
      try {
         viewport.defineInputPort(def);
      } catch (ModelErrorException ex) {
         build.errors.addError(ex.getMessage(),source);
         return;
      }
      // output
      if (!children.hasNext()) {
         build.errors.addError("Missing a "+DECLARE_OUTPUT_NM+" element.");
         return;
      }
      Element outputDecl = children.next();
      if (!outputDecl.getName().equals(DECLARE_OUTPUT_NM)) {
         build.errors.addError("Expecting a "+DECLARE_OUTPUT_NM+" element but found "+outputDecl.getName()+" instead.");
         return;
      }
      def = parseOutputDeclaration(build,outputDecl);
      if (def==null) {
         build.errors.addError("No output port definition was found.",outputDecl);
         return;
      }
      try {
         viewport.defineOutputPort(def);
      } catch (ModelErrorException ex) {
         build.errors.addError(ex.getMessage(),outputDecl);
         return;
      }
      
      // parameter* and steps
      boolean prologEnded = false;
      while (children.hasNext()) {
         Element decl = children.next();
         if (decl.getName().equals(PARAMETER_NM)) {
            String nameString = decl.getAttributeValue("name");
            if (nameString==null) {
               build.errors.addError("Missing 'name' attribute on parameter declaration.",decl.getBaseURI(),decl);
               continue;
            }
            Name name;
            try {
               name = decl.getNamespaceScope().expandName(nameString);
            } catch (XMLException xex) {
               build.errors.addError("Cannot expand parameter QName: "+xex.getMessage(),decl);
               continue;
            }
            String value = decl.getAttributeValue("value");
            if (value==null) {
               value = "";
            }
            parameters.put(name,value);
         } else {
            prologEnded = true;
            handler.handle(build,decl);
         }
      }
   }

   protected void parseForeach(BuildContext build,ForEach foreach,Element containerElement,ElementHandler handler) {
      Iterator<Element> children = containerElement.getElementChildren();
      Map<Name,String> parameters = foreach.getParameters();
      
      // viewport-source
      if (!children.hasNext()) {
         build.errors.addError("Missing a "+VIEWPORT_SOURCE_NM+" element.");
         return;
      }
      Element source = children.next();
      if (!source.getName().equals(ITERATION_SOURCE_NM)) {
         build.errors.addError("Expecting a "+ITERATION_SOURCE_NM+" element but found "+source.getName()+" instead.");
         return;
      }
      Reference ref = parseInput(build,source,false);
      if (ref==null) {
         build.errors.addError("No input definition was found.",source);
         return;
      }
      PortDefinition def = new PortDefinition(CURRENT_NM,false,ref);
      try {
         foreach.defineInputPort(def);
      } catch (ModelErrorException ex) {
         build.errors.addError(ex.getMessage(),source);
         return;
      }
      // parameter* and steps
      boolean prologEnded = false;
      while (children.hasNext()) {
         Element decl = children.next();
         if (decl.getName().equals(PARAMETER_NM)) {
            String nameString = decl.getAttributeValue("name");
            if (nameString==null) {
               build.errors.addError("Missing 'name' attribute on parameter declaration.",decl.getBaseURI(),decl);
               continue;
            }
            Name name;
            try {
               name = decl.getNamespaceScope().expandName(nameString);
            } catch (XMLException xex) {
               build.errors.addError("Cannot expand parameter QName: "+xex.getMessage(),decl);
               continue;
            }
            String value = decl.getAttributeValue("value");
            if (value==null) {
               value = "";
            }
            parameters.put(name,value);
         } else if (decl.getName().equals(DECLARE_OUTPUT_NM)) {
            def = parseOutputDeclaration(build,decl);
            if (def==null) {
               build.errors.addError("No output port definition was found.",decl);
               return;
            }
            try {
               foreach.defineOutputPort(def);
            } catch (ModelErrorException ex) {
               build.errors.addError(ex.getMessage(),decl);
               return;
            }
         } else {
            prologEnded = true;
            handler.handle(build,decl);
         }
      }
   }

   public static void main(String [] args) {
      DocumentLoader loader = new SAXDocumentLoader();
      PipelineModelBuilder builder = new PipelineModelBuilder();
      for (int i=0; i<args.length; i++) {
         File f = new File(args[i]);
         System.out.println(f.getAbsolutePath());
         try {
            Document doc = loader.load(f.toURI());
            try {
               PipelineModel model = builder.build(doc);
               model.resolve();
            } catch (ModelErrorException ex) {
               System.err.println(ex.getMessage());
            }
         } catch (IOException ex) {
            System.err.println(ex.getMessage());
         } catch (XMLException ex) {
            System.err.println(ex.getMessage());
         }
      }
   }
}
